package jmine.tec.script.impl;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

import jmine.tec.script.ScriptAPIMessages;
import jmine.tec.script.ScriptDispatcher;
import jmine.tec.script.ScriptDispatcherException;
import jmine.tec.script.ScriptExecutionContext;
import jmine.tec.script.ScriptExecutor;
import jmine.tec.script.ScriptExecutorException;
import jmine.tec.script.WrappingScriptExecutor;

/**
 * Implementação de Script Dispatcher que monta uma fila para cada tipo de ScriptExecutor fornecido com as instâncias daquele tipo.
 * 
 * @author jrenaut
 */
public class QueuedScriptDispatcher implements ScriptDispatcher {

    private final Map<Class<?>, BlockingQueue<ScriptExecutor>> executorQueues = new HashMap<Class<?>, BlockingQueue<ScriptExecutor>>();

    private final ThreadLocal<Map<Class<?>, ScriptExecutor>> localExecutors = new ThreadLocal<Map<Class<?>, ScriptExecutor>>();

    /**
     * Encontra um ScriptExecutor para este contexto e executa script retornando o resultado.
     * 
     * @param <R> o tipo do retorno
     * @param context ScriptExecutionContext
     * @return R
     * @throws ScriptExecutorException Se o script retornar erro
     */
    public <R> R dispatchExecution(final ScriptExecutionContext<R> context) throws ScriptExecutorException {
        if (context == null) {
            throw new NullPointerException("A null context was provided!");
        }
        Map<Class<?>, ScriptExecutor> map = this.localExecutorMap();
        final Class<? extends ScriptExecutor> executorClass = context.getScriptExecutorClass();
        ScriptExecutor scriptExecutor = map.get(executorClass);
        if (scriptExecutor != null) {
            // reuse executor associated with the current thread
            return scriptExecutor.execute(context);
        }
        BlockingQueue<ScriptExecutor> queue = this.getQueueFor(executorClass);
        if (queue != null) {
            try {
                ScriptExecutor executor = queue.take();
                map.put(executorClass, executor);
                try {
                    return executor.execute(context);
                } finally {
                    map.remove(executorClass);
                    if (map.isEmpty()) {
                        this.localExecutors.remove();
                    }
                    if (!queue.offer(executor)) {
                        throw new AssertionError(
                                "Executors must always be returned to their queues! Failure to do so indicates an abnormal state!");
                    }
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                throw new ScriptDispatcherException(ScriptAPIMessages.ERROR_INTERRUPTED.create(), e);
            }
        } else {
            throw new ScriptDispatcherException(ScriptAPIMessages.ERROR_EXECUTOR_NOTFOUND.create(executorClass));
        }
    }

    /**
     * Encontra um ScriptExecutor para este contexto e executa script recuperando as dependências.
     * 
     * @param <R> o tipo do retorno
     * @param context ScriptExecutionContext
     * @return R
     * @throws ScriptExecutorException Se o script retornar erro
     */
    public <R> R dispatchGetDependencies(final ScriptExecutionContext<R> context) throws ScriptExecutorException {
        if (context == null) {
            throw new NullPointerException("A null context was provided!");
        }
        Map<Class<?>, ScriptExecutor> map = this.localExecutorMap();
        final Class<? extends ScriptExecutor> executorClass = context.getScriptExecutorClass();
        ScriptExecutor scriptExecutor = map.get(executorClass);
        if (scriptExecutor != null) {
            // reuse executor associated with the current thread
            return scriptExecutor.getDependencies(context);
        }
        BlockingQueue<ScriptExecutor> queue = this.getQueueFor(executorClass);
        if (queue != null) {
            try {
                ScriptExecutor executor = queue.take();
                map.put(executorClass, executor);
                try {
                    return executor.getDependencies(context);
                } finally {
                    map.remove(executorClass);
                    if (map.isEmpty()) {
                        this.localExecutors.remove();
                    }
                    if (!queue.offer(executor)) {
                        throw new AssertionError(
                                "Executors must always be returned to their queues! Failure to do so indicates an abnormal state!");
                    }
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                throw new ScriptDispatcherException(ScriptAPIMessages.ERROR_INTERRUPTED.create(), e);
            }
        } else {
            throw new ScriptDispatcherException(ScriptAPIMessages.ERROR_EXECUTOR_NOTFOUND.create(executorClass));
        }
    }

    /**
     * Devovle os erros de validação para o contexto dado.
     * 
     * @param <R> O tipo do resultado
     * @param context o contexto de execução
     * @return R
     * @throws ScriptExecutorException Se um erro ocorrer no dispatch
     */
    public <R> R dispatchGetValidations(final ScriptExecutionContext<R> context) throws ScriptExecutorException {
        if (context == null) {
            throw new NullPointerException("A null context was provided!");
        }
        Map<Class<?>, ScriptExecutor> map = this.localExecutorMap();
        final Class<? extends ScriptExecutor> executorClass = context.getScriptExecutorClass();
        ScriptExecutor scriptExecutor = map.get(executorClass);
        if (scriptExecutor != null) {
            // reuse executor associated with the current thread
            return scriptExecutor.getValidations(context);
        }
        BlockingQueue<ScriptExecutor> queue = this.getQueueFor(executorClass);
        if (queue != null) {
            try {
                ScriptExecutor executor = queue.take();
                map.put(executorClass, executor);
                try {
                    return executor.getValidations(context);
                } finally {
                    map.remove(executorClass);
                    if (map.isEmpty()) {
                        this.localExecutors.remove();
                    }
                    if (!queue.offer(executor)) {
                        throw new AssertionError(
                                "Executors must always be returned to their queues! Failure to do so indicates an abnormal state!");
                    }
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                throw new ScriptDispatcherException(ScriptAPIMessages.ERROR_INTERRUPTED.create(), e);
            }
        } else {
            throw new ScriptDispatcherException(ScriptAPIMessages.ERROR_EXECUTOR_NOTFOUND.create(executorClass));
        }
    }

    /**
     * Devolve o mapa de executores local
     * 
     * @return {@link Map}
     */
    private Map<Class<?>, ScriptExecutor> localExecutorMap() {
        Map<Class<?>, ScriptExecutor> map = this.localExecutors.get();
        if (map == null) {
            map = new HashMap<Class<?>, ScriptExecutor>();
            this.localExecutors.set(map);
        }
        return map;
    }

    /**
     * Seta os executors com os quais as filas serão populadas.
     * 
     * @param executors ScriptExecutor[]
     */
    public synchronized void setScriptExecutors(final ScriptExecutor... executors) {
        if (!this.executorQueues.isEmpty()) {
            throw new IllegalStateException("Queues already created!");
        }

        Map<Class<?>, List<ScriptExecutor>> classifiedExecutors = new HashMap<Class<?>, List<ScriptExecutor>>();

        for (ScriptExecutor executor : executors) {
            if (executor == null) {
                throw new NullPointerException("A null script executor was provided!");
            }
            final Class<?> type = this.getExecutorKey(executor);

            List<ScriptExecutor> list = classifiedExecutors.get(type);
            if (list == null) {
                list = new ArrayList<ScriptExecutor>();
                classifiedExecutors.put(type, list);
            }
            list.add(executor);
        }

        for (Entry<Class<?>, List<ScriptExecutor>> entry : classifiedExecutors.entrySet()) {
            BlockingQueue<ScriptExecutor> queue = new ArrayBlockingQueue<ScriptExecutor>(entry.getValue().size(), false, entry.getValue());
            this.executorQueues.put(entry.getKey(), queue);
        }
    }

    /**
     * Encontra a chave do executor.
     * 
     * @param executor executor
     * @return Class
     */
    protected Class<? extends ScriptExecutor> getExecutorKey(ScriptExecutor executor) {
        final Class<? extends ScriptExecutor> type =
                executor instanceof WrappingScriptExecutor ? ((WrappingScriptExecutor) executor).getWrappedExecutorType() : executor
                        .getClass();
        return type;
    }

    /**
     * Retorna a fila para este tipo de ScriptExecutor.
     * 
     * @param clazz A classe do ScriptExecutor
     * @return A fila para este tipo de ScriptExecutor (pode ser null)
     */
    private synchronized BlockingQueue<ScriptExecutor> getQueueFor(final Class<? extends ScriptExecutor> clazz) {
        return this.executorQueues.get(clazz);
    }

}
